相信很多小伙伴对redis的持久化保证有疑问:

  • redis不是内存性应用吗?为什么磁盘拥堵的情况下会影响读写呢?
  • redis不是支持持久化吗?为什么会丢数据?
  • redis的持久化数据安全吗?到什么级别?能不能取代数据库?

所以简单整理了这篇文章,对redis的持久化内部原理进行分析,同时可为其它需要持久化的实现提供参考。 鉴于OS的差异,本文统一以linux 2.6+为准。

Redis的持久化工作原理

Redis的持久化有RDB和AOF两种,RDB 可以定时备份内存中的数据集。服务器启动的时候,可以从 RDB 文件中回复数据集。AOF 可以记录服务器的所有写操作。在服务器重新启动的时候,会把所有的写操作重新执行一遍,从而实现数据备份。当写操作集过大(比原有的数据集还大),redis会重写写操作集。因为每次RDB都保存全量数据,这是一个开销很大的操作,为了避免进行RDB时fork对主进程影响,以及尽量减少发生故障时丢失的数据量,一般情况大家采用数据持久化策略是AOF。
下面先来看一下AOF数据组织方式 假设redis中有foo:helloworld的string类型的key,那么进行AOF持久化后,appendonly.aof文件有如下内容:

  

*2         # 表示这条命令的消息体共2行
$6         # 下一行的数据长度为6
SELECT     # 消息体
$1         # 下一行数据长度为1
0          # 消息体
*3         # 表示这条命令的消息体共2行
$3         # 下一行的数据长度为3
set        # 消息体
$3         # 下一行的数据长度为3
foo        # 消息体
$10        # 下一行的数据长度为10
helloworld # 消息体

通过解析上面内容,能得到熟悉的一条redis命令:SELECT 0; SET foo helloworld 我们可以通过执行命令:BGREWRITEAOF实现一次aof文件的重写,这时redis会将内存中每一个key按照上面格式写入磁盘上appendonly.aof文件; 而当Redis启动载入这个AOF文件时,会创建用于执行AOF文件包含Redis命令的伪客户端,并在载入完成后关闭这个伪客户端。另外,因为AOF持久化是通过记录写命令流水来记录数据变化,这个文件会越来越大,为了解决这个问题,Redis提供了AOF重写功能,通过将当期内存中数据导出创建一个新的AOF文件替换现有AOF文件,这样新文件的体积就会小很多,具体机制这里就不过多展开了。

文件IO相关原理

在进入redis的具体实现之前,我们先梳理一下文件IO相关函数及系统实现。这里涉及的文件IO操作有如下几个:

open

int open(const char path, int oflag, ... / mode_t mode */ );
path参数是要打开的文件名,oflag参数指定一个或多个选项,例如:

O_WRONLY 只写打开
O_APPEND 每次写操作之前,将文件偏移量设置在文件的当前结尾处,在一处成功写之后,该文件的偏移量增加时间写的字节数。
O_CREAT 如果文件不存在则先创建它。
write

ssizet write(int fd, const void *buf, sizet nbytes);
write向打开的文件写数据,返回值通常与nbytes值相同,否则表示出错

ftruncate

int ftruncate(int fd, off_t length);
通过ftruncate可以将文件长度截短或是增长,如果length小于原来长度,超过length的数据就不能在访问,如果大于原来长度,文件长度则增加,如果之前文件尾端到length长度之间没有数据则读出的为0,相当于在文件中创建了空洞

在我们向文件中写数据时,传统Unix/Liunx系统内核通常现将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写(delayed write)。对磁盘文件的write操作,更新的只是内存中的page cache,因为write调用不会等到硬盘IO完成之后才返回,因此如果OS在write调用之后、硬盘同步之前崩溃,则数据可能丢失。为了保证磁盘上时间文件系统与缓冲区内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数:

sync
只是将所有修改过的块缓冲区排查写队列,然后就返回,它并不等待时间写磁盘操作结束。通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期flush内核的块缓冲区。
fsync
只对由文件描述符fd指定的单一文件起作用,并且等待写磁盘操作结束才返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保将修改过的块立即写到磁盘上。
fdatasync
类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。

  现在来看一下fsync的性能问题,与fdatasync不同,fsync除了同步文件的修改内容(脏页),fsync还会同步文件的描述信息(metadata,包括size、访问时间statime & stmtime等等),因为文件的数据和metadata通常存在硬盘的不同地方,因此fsync至少需要两次IO写操作,这个在fsync的man page有说明:
Applications that access databases or log files often write a tiny data fragment (e.g., one line in a log file) and then call fsync()
immediately in order to ensure that the written data is physically
stored on the harddisk. Unfortunately, fsync() will always initiate
two write operations: one for the newly written data and another one
in order to update the modification time stored in the inode. If the
modification time is not a part of the transaction concept fdatasync()
can be used to avoid unnecessary inode disk write operations.
fdatasync不会同步metadata,因此可以减少一次IO写操作。fdatasync的man page中的解释:

fdatasync() is similar to fsync(), but does not flush modified
metadata unless that metadata is needed in order to allow a subsequent
data retrieval to be correctly handled. For example, changes to
st_atime or st_mtime (respectively, time of last access and time of
last modification; see stat(2)) do not require flushing because they
are not necessary for a subsequent data read to be handled correctly.
On the other hand, a change to the file size (st_size, as made by say
ftruncate(2)), would require a metadata flush. The aim of fdatasync()
is to reduce disk activity for applications that do not require all
metadata to be synchronized with the disk.

具体来说,如果文件的尺寸(st_size)发生变化,是需要立即同步,否则OS一旦崩溃,即使文件的数据部分已同步,由于metadata没有同步,依然读不到修改的内容。而最后访问时间(atime)/修改时间(mtime)是不需要每次都同步的,只要应用程序对这两个时间戳没有苛刻的要求,基本没有影响。在Redis的源文件src/config.h中可以看到在Redis针对Linux实际使用了fdatasync()来进行刷盘操作

源文件:src/config.h

 91 #ifdef __linux__
 92 #define aof_fsync fdatasync
 93 #else
 94 #define aof_fsync fsync
 95 #endif

Redis的AOF刷盘工作原理

Redis是通过apendfsync参数来设置不同刷盘策略,apendfsync主要有下面三个选项:

always
每次有新命令追加到AOF文件是就执行一次同步到AOF文件的操作,安全性最高,但是性能影响最大。
everysec
每秒执行一次同步到AOF文件的操作,redis会在一个单独线程中执行同步操作。
no
将数据同步操作交给操作系统来处理,性能最好,但是数据可靠性最差。 加入在配置文件设置appendonly=yes后,没有指定apendfsync,默认会使用everysec选项,一般都是采用的这个选项。
下面我们来具体分析一下Redis代码中关于AOF刷盘操作的工作原理:

在appendonly yes激活AOF时,会调用startAppendOnly()函数来打开appendonly.aof文件句柄。

241 server.aof_fd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
同时在Redis启动时也会创建专门的bio线程处理aof持久化,在src/server.c文件的initServer()中会调用bioInit()函数创建两个线程,分别用来处理刷盘和关闭文件的任务。代码如下:

源文件:src/bio.h

38 /* Background job opcodes */
39 #define BIO_CLOSE_FILE    0 /* Deferred close(2) syscall. */
40 #define BIO_AOF_FSYNC     1 /* Deferred AOF fsync. */
41 #define BIO_NUM_OPS       2

源文件: src/bio.c

116     for (j = 0; j < BIO_NUM_OPS; j++) {
117         void *arg = (void*)(unsigned long) j;
118         if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {
119             serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");
120             exit(1);
121         }
122         bio_threads[j] = thread;
123     }
  当redis服务器执行写命令时,例如SET foo helloworld,不仅仅会修改内存数据集,也会记录此操作,记录的方式就是前面所说的数据组织方式。redis将一些内容被追加到server.aofbuf缓冲区中,可以把它理解为一个小型临时中转站,所有累积的更新缓存都会先放入这里,它会在特定时机写入文件或者插入到server.aofrewritebufblocks,同时每次写操作后先写入缓存,然后定期fsync到磁盘,在到达某些时机(主要是受auto-aof-rewrite-percentage/auto-aof-rewrite-min-size这两个参数影响)后,还会fork子进程执行rewrite。为了避免在服务器突然崩溃时丢失过多的数据,在redis会在下列几个特定时机调用flushAppendOnlyFile函数进行写盘操作:

进入事件循环之前
服务器定时函数serverCron()中,在Redis运行期间主要是在这里调用flushAppendOnlyFile
停止AOF策略的stopAppendOnly()函数中
注:因 serverCron 函数中的所有代码每秒都会调用 server.hz 次,为了对部分代码的调用次数进行限制,Redis使用了一个宏 runwithperiod(milliseconds) { ... } ,这个宏可以将被包含代码的执行次数降低为每 milliseconds 执行一次。

源文件: src/server.c

 1099 int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
 1260     /* AOF postponed flush: Try at every cron cycle if the slow fsync
 1261      * completed. */
 1262     if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);
 1263
 1264     /* AOF write errors: in this case we have a buffer to flush as well and
 1265      * clear the AOF error in case of success to make the DB writable again,
 1266      * however to try every second is enough in case of 'hz' is set to
 1267      * an higher frequency. */
 1268     run_with_period(1000) {
 1269         if (server.aof_last_write_status == C_ERR)
 1270             flushAppendOnlyFile(0);
 1271     }
 1316 }
  通过下面的代码可以看到flushAppendOnlyFile函数中,在write写盘之后根据apendfsync选项来执行刷盘策略,如果是AOFFSYNCALWAYS,就立即执行刷盘操作,如果是AOFFSYNCEVERYSEC,则创建一个后台异步刷盘任务。 在函数bioCreateBackgroundJob()会创建bio后台任务,在函数bioProcessBackgroundJobs()会执行bio后台任务的处理。

源文件:src/aof.c

 200 // 调用bio的创建异步线程任务函数,添加后台刷盘任务
 201 void aof_background_fsync(int fd) {
 202     bioCreateBackgroundJob(BIO_AOF_FSYNC,(void*)(long)fd,NULL,NULL);
 203 }
 
  238 int startAppendOnly(void) {
 239     char cwd[MAXPATHLEN];
 240     // 通过appendonly yes激活AOF时,会调用startAppendOnly()函数来打开appendonly.aof文件句柄。
 241     server.aof_last_fsync = server.unixtime;
 242     server.aof_fd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
 243     serverAssert(server.aof_state == AOF_OFF);
 244     if (server.aof_fd == -1) {
 245         char *cwdp = getcwd(cwd,MAXPATHLEN);
 246
 247         serverLog(LL_WARNING,
 248             "Redis needs to enable the AOF but can't open the "
 249             "append only file %s (in server root dir %s): %s",
 250             server.aof_filename,
 251             cwdp ? cwdp : "unknown",
 252             strerror(errno));
 253         return C_ERR;
 254     }
 255     if (server.rdb_child_pid != -1) {
 256         server.aof_rewrite_scheduled = 1;
 257         serverLog(LL_WARNING,"AOF was enabled but there is already a child process saving an RDB file on disk. An AOF background was scheduled to start when possible.");
 258     } else if (rewriteAppendOnlyFileBackground() == C_ERR) {
 259         close(server.aof_fd);
 260         serverLog(LL_WARNING,"Redis needs to enable the AOF but can't trigger a background AOF rewrite operation. Check the above logs for more info about the error.");
 261         return C_ERR;
 262     }
 263     /* We correctly switched on AOF, now wait for the rewrite to be complete
 264      * in order to append data on disk. */
 265     server.aof_state = AOF_WAIT_REWRITE;
 266     return C_OK;
 267 }
 
     // 执行write和fsync操作
 288 void flushAppendOnlyFile(int force) {
 289     ssize_t nwritten;
 290     int sync_in_progress = 0;
 291     mstime_t latency;
 292     // 没有数据,无需写盘
 293     if (sdslen(server.aof_buf) == 0) return;
 294     /* 通过bio的任务计数器bio_pending来判断是否有后台fsync操作正在进行
          * 如果有就要标记下sync_in_progress
          */
 295     if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
 296         sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0;
 297     /* 如果没有设置强制刷盘的选项,可能不会立即进行,而是延迟执行AOF刷盘
          * 因为 Linux 上的 write(2) 会被后台的 fsync 阻塞, 如果强制执行 
          * write 的话,服务器主线程将阻塞在 write 上面
          */         
 298     if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
 302         if (sync_in_progress) {
 303             if (server.aof_flush_postponed_start == 0) {
 306                 server.aof_flush_postponed_start = server.unixtime;
 307                 return;
                 // 如果距离上次执行刷盘操作没有超过2秒,直接返回,
 308             } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
 311                 return;
 312             }
                 /* 如果后台还有 fsync 在执行,并且 write 已经推迟 >= 2 秒
                  * 那么执行写操作(write 将被阻塞)
                  * 假如此时出现死机等故障,可能存在丢失2秒左右的AOF日志数据
                  */              
 315             server.aof_delayed_fsync++;
 316             serverLog(LL_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down      Redis.");
 317         }
 318     }
 324     // 将server.aof_buf中缓存的AOF日志数据进行写盘
 325     latencyStartMonitor(latency);
 326     nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
 327     latencyEndMonitor(latency);
         // 重置延迟刷盘时间
 343     server.aof_flush_postponed_start = 0;
 344     // 如果write失败,那么尝试将该情况写入到日志里面
 345     if (nwritten != (signed)sdslen(server.aof_buf)) {
 346         static time_t last_write_error_log = 0;
 347         int can_log = 0;
 348
 350         if ((server.unixtime - last_write_error_log) > AOF_WRITE_LOG_ERROR_RATE) {
 351             can_log = 1;
 352             last_write_error_log = server.unixtime;
 353         }
 354
 356         if (nwritten == -1) {
 357             if (can_log) {
 358                 serverLog(LL_WARNING,"Error writing to the AOF file: %s",
 359                     strerror(errno));
 360                 server.aof_last_write_errno = errno;
 361             }
 362         } else {
 363             if (can_log) {
 364                 serverLog(LL_WARNING,"Short write while writing to "
 365                                        "the AOF file: (nwritten=%lld, "
 366                                        "expected=%lld)",
 367                                        (long long)nwritten,
 368                                        (long long)sdslen(server.aof_buf));
 369             }
 370             // 通过ftruncate尝试删除新追加到AOF中的不完整的数据内容
 371             if (ftruncate(server.aof_fd, server.aof_current_size) == -1) {
 372                 if (can_log) {
 373                     serverLog(LL_WARNING, "Could not remove short write "
 374                              "from the append-only file.  Redis may refuse "
 375                              "to load the AOF the next time it starts.  "
 376                              "ftruncate: %s", strerror(errno));
 377                 }
 378             } else {
 381                 nwritten = -1;
 382             }
 383             server.aof_last_write_errno = ENOSPC;
 384         }
             // 处理写入AOF文件是出现的错误
 387         if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
 392             serverLog(LL_WARNING,"Can't recover from AOF write error when the AOF fsync policy is 'always'. Exiting...");
 393             exit(1);
 394         } else {
 398             server.aof_last_write_status = C_ERR;
                 // 如果是已经写入了部分数据,是不能通过ftruncate进行撤销的
                 // 这里通过sdsrange清除掉aof_buf中已经写入磁盘的那部分数据
 402             if (nwritten > 0) {
 403                 server.aof_current_size += nwritten;
 404                 sdsrange(server.aof_buf,nwritten,-1);
 405             }
 406             return; 
 407         }
 408     } else {
 411         if (server.aof_last_write_status == C_ERR) {
 412             serverLog(LL_WARNING,
 413                 "AOF write error looks solved, Redis can write again.");
 414             server.aof_last_write_status = C_OK;
 415         }
 416     }
         // 更新写入后的 AOF 文件大小
 417     server.aof_current_size += nwritten;
 418
 419      /* 当 server.aof_buf 足够小,重新利用空间,防止频繁的内存分配。
           * 相反,当 server.aof_buf 占据大量的空间,采取的策略是释放空间。
           */
 420      
 421     if ((sdslen(server.aof_buf)+sdsavail(server.aof_buf)) < 4000) {
 422         sdsclear(server.aof_buf);
 423     } else {
 424         sdsfree(server.aof_buf);
 425         server.aof_buf = sdsempty();
 426     }
 427
 428     /* 如果 no-appendfsync-on-rewrite 选项激活状态
 429      * 并有BGSAVE或BGREWRITEAOF正在进行,那么不执行fsync
          */
 430     if (server.aof_no_fsync_on_rewrite &&
 431         (server.aof_child_pid != -1 || server.rdb_child_pid != -1))
 432             return;
 433
 434     // 执行 fysnc
 435     if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
 436         /* aof_fsync is defined as fdatasync() for Linux in order to avoid
 437          * flushing metadata. */
 438         latencyStartMonitor(latency);
 439         aof_fsync(server.aof_fd); /* Let's try to get this data on the disk */
 440         latencyEndMonitor(latency);
 441         latencyAddSampleIfNeeded("aof-fsync-always",latency);
 442         server.aof_last_fsync = server.unixtime;
 443     } else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
 444                 server.unixtime > server.aof_last_fsync)) {
 445         if (!sync_in_progress) aof_background_fsync(server.aof_fd);
 446         server.aof_last_fsync = server.unixtime;
 447     }
 448 }
 449
 

最后我们重新回顾一下关于aof的写盘操作:

主线程操作完内存数据后,会执行write,之后根据配置决定是立即还是延迟fdatasync
redis在启动时,会创建专门的bio线程用于处理aof持久化
如果是apendfsync=everysec,时机到达后,会创建异步任务(bio)
bio线程轮询任务池,拿到任务后同步执行fdatasync

结论:

关于数据可靠性:

如果是always每次写命令后都是刷盘,故障时丢失数据最少,如果是everysec,会丢失大概2秒的数据,在bio延迟刷盘时如果后台刷盘操作卡住,在ServerCron里面每一轮循环(频率取决于hz参数,我们设置为100,也就是一秒执行100次循环)都检查是否上一次后台刷盘操作是否超过2秒,如果超过立即进行一次强制刷盘,因此可以粗略的认为最大可能丢失2.01秒的数据。
如果在进行bgrewriteaof期间出现故障,因rewrite会阻塞fdatasync刷盘,可能丢失的数据量更大,这个就不太容易量化评估了。

关于aof对延迟的影响

关于AOF对访问延迟的影响,Redis作者曾经专门写过一篇博客 fsync() on a different thread: apparently a useless trick,结论是bio对延迟的改善并不是很大,因为虽然apendfsync=everysec时fdatasync在后台运行,wirte的aof_buf并不大,基本上不会导致阻塞,而是后台的fdatasync会导致write等待datasync完成了之后才调用write导致阻塞,fdataysnc会握住文件句柄,fwrite也会用到文件句柄,这里write会导致了主线程阻塞。这也就是为什么之前浪潮服务器的RAID出现性能问题时,虽然对大部分应用没有影响,但是对于Redis这种对延迟非常敏感的应用却造成了影响的原因。

是否可以关闭AOF?

既然开启AOF会造成访问延迟,那么是可以关闭呢,答案是肯定的,对应纯缓存场景,例如数据Missed后会自动访问数据库,或是可以快速从数据库重建的场景,完全可以关闭,从而获取最优的性能。其实即使关闭了AOF也不意味着当一个分片实例Crash时会丢掉这个分片的数据,我们实际生产环境中每个分片都是会有主备(Master/Slave)两个实例,通过Redis的Replication机制保持同步,当主实例Crash时会自动进行主从切换,将备实例切换为主,从而保证了数据可靠性,为了避免主备同时Crash,实际生产环境都是将主从分布在不同物理机和不同交换机下。

Redis的持久化是否具备数据库能力

目前还不能代替数据库,更不具备关系型数据库的功能,如果是对数据可靠性要求高的业务需要慎重,建议考虑使用基于RocksDB的解决方案


窦锦帅
1 声望0 粉丝